Skip to content

Conversation

@randygrok
Copy link
Contributor

@randygrok randygrok commented Jan 28, 2026

Description

Closes: #109

@claude
Copy link

claude bot commented Jan 28, 2026

Claude finished @randygrok's task —— View job


Code Review Analysis

I've completed a comprehensive review of this client implementation for EvNode transactions. Here's my detailed feedback:

  • Read and analyze core client implementation files
  • Review code quality and architecture
  • Check for potential bugs and security issues
  • Evaluate performance considerations
  • Assess test coverage and quality
  • Provide comprehensive feedback with specific recommendations

Overall Assessment

This is a well-structured implementation of a Viem client for custom EvNode transactions. The code demonstrates good TypeScript practices, comprehensive RLP encoding/decoding, and thoughtful API design. However, there are several important issues that should be addressed.

Critical Issues

🔴 Security: eth_sign Method Risks

Location: clients/src/signing.ts:51-69

The hashSignerFromRpcClient function uses eth_sign which has inconsistent behavior across Ethereum clients:

  • Geth: Signs raw hash as expected
  • Parity/OpenEthereum: Applies EIP-191 prefix, breaking signatures
  • Risk: Silent failures with certain RPC providers

Recommendation: Add prominent documentation warning about node compatibility requirements or implement provider detection.

🔴 Gas Estimation Incomplete

Location: clients/src/encoding.ts:115-132

The estimateIntrinsicGas function has significant gaps:

  1. Missing access list costs: Doesn't account for EIP-2930 access list gas (2400 per address + 1900 per storage key)
  2. Wrong base calculation: Currently adds base gas (21000) for each call, but it should be base + per-call overhead

Correct formula should be:

// Base (21000) + per-call overhead + CREATE gas + data gas + access list gas

🔴 Error Handling Too Broad

Location: clients/src/client.ts:228-236

The fetchMaxPriorityFee function catches ALL errors and defaults to 0n, potentially hiding:

  • Network connectivity issues
  • RPC misconfigurations
  • Authentication problems

Should only catch specific "method not supported" errors.

Performance & Quality Issues

⚠️ Gas Price Strategy Suboptimal

Location: clients/src/client.ts:199

Using eth_gasPrice as fallback for maxFeePerGas can cause overpayment. EIP-1559 best practice:

maxFeePerGas = baseFeePerGas * 2 + maxPriorityFeePerGas

⚠️ Code Organization

Location: clients/src/index.ts

The previous index.ts had all implementation in one file. Good that it's been refactored into separate modules (client.ts, encoding.ts, signing.ts, types.ts).

⚠️ Magic Numbers

Location: Multiple files

Several magic numbers should be constants:

  • 21000nBASE_TX_GAS ✅ (already done in encoding.ts)
  • 32000nCREATE_GAS ✅ (already done in encoding.ts)
  • 11EVNODE_TX_FIELD_COUNT ✅ (already done in encoding.ts)

Architecture Strengths

✅ Excellent Type Safety

  • Comprehensive TypeScript types for all EvNode transaction structures
  • Proper separation between signed/unsigned transactions
  • Good use of Viem's type system

✅ Robust RLP Encoding/Decoding

  • Handles all transaction fields correctly
  • Proper signature normalization
  • Good error handling for malformed data

✅ Clean API Design

  • Intuitive client interface with send(), createIntent(), sponsorIntent()
  • Good separation between executor and sponsor signing
  • Flexible configuration with default signers

✅ Comprehensive Testing

  • Strong unit test coverage for core functions
  • Good integration test examples
  • Proper CI setup for automated testing

Security Assessment

✅ Signature Handling

  • Proper signature normalization and validation
  • Correct domain separation for executor vs sponsor signatures
  • Safe address recovery implementation

✅ Input Validation

  • Good validation of transaction structure (validateEvNodeTx)
  • Proper hex string validation throughout
  • Safe BigInt conversions

⚠️ Missing Access List Validation

The code should validate access list format and potentially warn about gas estimation impacts.

Test Coverage Analysis

✅ Strong Unit Testing

  • Comprehensive tests for encoding/decoding roundtrips
  • Good signature verification tests
  • Proper error case testing

⚠️ Integration Test Gaps

  • No tests for different RPC provider behaviors
  • Missing tests for gas estimation edge cases
  • Could benefit from more error scenario testing

Recommendations

High Priority

  1. Fix gas estimation to include access list costs
  2. Add eth_sign compatibility warnings in documentation
  3. Improve error handling in RPC calls to be more specific
  4. Update gas price strategy for EIP-1559 best practices

Medium Priority

  1. Add access list validation
  2. Expand integration tests for edge cases
  3. Consider adding transaction simulation before sending
  4. Add more robust RPC provider detection

Low Priority

  1. Add performance benchmarks for encoding/decoding
  2. Consider adding transaction debugging utilities
  3. Explore adding automatic gas price optimization

Final Verdict

This is a solid foundation for an EvNode transaction client. The architecture is well-designed, the type safety is excellent, and the core functionality is implemented correctly. The main concerns are around gas estimation accuracy and RPC provider compatibility, which are fixable issues that don't affect the fundamental design.

Recommendation: Address the critical gas estimation and error handling issues, then this implementation will be production-ready.

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @randygrok, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request establishes the foundational client-side logic for a new EvNode transaction system. It defines the structure and behavior of these specialized transactions, enabling developers to programmatically construct, sign with multiple parties (executor and sponsor), and dispatch them to the network. The implementation focuses on robust data handling, signature management, and integration with existing blockchain interaction libraries to streamline the development of applications utilizing this new transaction type.

Highlights

  • New EvNode Transaction Type: Introduces a custom EvNode transaction type (0x76) with specific structures for calls, access lists, and dual signatures (executor and optional fee payer/sponsor).
  • Comprehensive Client Library: Provides a full client implementation (createEvnodeClient) for building, signing, and sending these custom transactions, including functions for creating sponsorable intents and handling sponsor signatures.
  • RLP Encoding and Decoding: Implements robust RLP encoding and decoding mechanisms for EvNode transactions, ensuring proper serialization and deserialization for on-chain submission and off-chain processing.
  • Viem Integration: Seamlessly integrates with the viem library, leveraging its utilities for address recovery, hash computation, and RPC interactions, and registers the EvNode transaction type with viem's transaction serializer.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a Viem client for a custom EvNode transaction type. The implementation is comprehensive, covering transaction encoding/decoding, signing, and client-side actions. My review focuses on improving robustness, correctness of gas estimations, and maintainability. I've identified a few areas for improvement, including more accurate gas calculations, safer error handling in RPC calls, and addressing potential issues with the eth_sign method. I've also suggested some minor refactoring to reduce code duplication.

Comment on lines 405 to 423
export function hashSignerFromRpcClient(
client: Client,
address: Address,
): HashSigner {
return {
address,
signHash: async (hash) => {
// eth_sign is expected to sign raw bytes (no EIP-191 prefix).
const signature = await client.request({
method: 'eth_sign',
params: [address, hash],
});
if (!isHex(signature)) {
throw new Error('eth_sign returned non-hex signature');
}
return signature;
},
};
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The eth_sign RPC method has inconsistent behavior across different Ethereum node implementations and is generally considered deprecated. Some nodes (like Geth) will sign the raw hash as expected here, but others (like Parity/OpenEthereum) will prefix the hash according to EIP-191, which would lead to invalid signatures for this use case. This can cause hard-to-debug issues for users of your client depending on which RPC provider they use.

It's highly recommended to add a prominent warning in the function's documentation about this limitation and the specific node behavior it relies on.

Comment on lines 608 to 616
async function fetchMaxPriorityFee(client: Client): Promise<bigint> {
try {
const result = await client.request({ method: 'eth_maxPriorityFeePerGas' });
if (!isHex(result)) throw new Error('eth_maxPriorityFeePerGas returned non-hex');
return hexToBigIntSafe(result);
} catch {
return 0n;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The broad catch block here will swallow any error from the eth_maxPriorityFeePerGas call and default to 0n. This can hide underlying problems like network connectivity issues or RPC misconfigurations, leading to transactions being sent with a potentially undesirable maxPriorityFeePerGas. It's safer to only catch specific errors that indicate the method is not supported (like on a pre-EIP-1559 chain) and re-throw others.

  try {
    const result = await client.request({ method: 'eth_maxPriorityFeePerGas' });
    if (!isHex(result)) throw new Error('eth_maxPriorityFeePerGas returned non-hex');
    return hexToBigIntSafe(result);
  } catch (err) {
    // Only fallback to 0n if the method is not supported.
    // A robust implementation would check for a specific RPC error code (e.g., -32601).
    if (err instanceof Error && (err.message.includes('not found') || err.message.includes('not supported'))) {
      return 0n;
    }
    throw err;
  }

Comment on lines 205 to 217
export function estimateIntrinsicGas(calls: Call[]): bigint {
let gas = 21000n;

for (const call of calls) {
if (call.to === null) gas += 32000n;

for (const byte of hexToBytes(call.data)) {
gas += byte === 0 ? 4n : 16n;
}
}

return gas;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current implementation of estimateIntrinsicGas does not account for the gas costs associated with an accessList (per EIP-2930). This will lead to an underestimation of the intrinsic gas for transactions using access lists, potentially causing them to fail. The function should be updated to accept an accessList and include its cost in the calculation.

export function estimateIntrinsicGas(calls: Call[], accessList?: AccessList): bigint {
  let gas = 21000n;

  for (const call of calls) {
    if (call.to === null) gas += 32000n;

    for (const byte of hexToBytes(call.data)) {
      gas += byte === 0 ? 4n : 16n;
    }
  }

  if (accessList) {
    const ACCESS_LIST_ADDRESS_COST = 2400n;
    const ACCESS_LIST_STORAGE_KEY_COST = 1900n;
    gas += BigInt(accessList.length) * ACCESS_LIST_ADDRESS_COST;
    for (const item of accessList) {
      gas += BigInt(item.storageKeys.length) * ACCESS_LIST_STORAGE_KEY_COST;
    }
  }

  return gas;
}

Comment on lines 233 to 242
async sendEvNodeTransaction(args: {
calls: Call[];
executor: HashSigner;
chainId?: bigint;
nonce?: bigint;
maxFeePerGas?: bigint;
maxPriorityFeePerGas?: bigint;
gasLimit?: bigint;
accessList?: AccessList;
}): Promise<Hex> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The args parameter uses an inline anonymous type that is very similar to the EvnodeSendArgs interface defined earlier. This creates code duplication and can make maintenance harder.

To improve this, consider defining a specific type for these arguments that extends EvnodeSendArgs but makes executor a required property. This would make the code more DRY and easier to reason about.

Comment on lines 273 to 282
async createSponsorableIntent(args: {
calls: Call[];
executor: HashSigner;
chainId?: bigint;
nonce?: bigint;
maxFeePerGas?: bigint;
maxPriorityFeePerGas?: bigint;
gasLimit?: bigint;
accessList?: AccessList;
}): Promise<SponsorableIntent> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Similar to sendEvNodeTransaction, this function uses an inline anonymous type for its args parameter which duplicates most of EvnodeIntentArgs. Reusing and extending the existing interface would make the code more maintainable and reduce redundancy.

const nonce = overrides.nonce ?? (await fetchNonce(client, address));
const maxPriorityFeePerGas =
overrides.maxPriorityFeePerGas ?? (await fetchMaxPriorityFee(client));
const maxFeePerGas = overrides.maxFeePerGas ?? (await fetchGasPrice(client));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Defaulting maxFeePerGas to the result of eth_gasPrice can be suboptimal for EIP-1559 transactions, as it might lead to overpaying for gas. A more conventional approach is to calculate maxFeePerGas as baseFeePerGas + maxPriorityFeePerGas. This would require fetching the baseFeePerGas from the latest block.

const maxPriorityFeePerGas =
overrides.maxPriorityFeePerGas ?? (await fetchMaxPriorityFee(client));
const maxFeePerGas = overrides.maxFeePerGas ?? (await fetchGasPrice(client));
const gasLimit = overrides.gasLimit ?? estimateIntrinsicGas(calls);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To fix the underestimation of intrinsic gas, you should pass the accessList to estimateIntrinsicGas here, assuming you've updated the function as per my other comment.

  const gasLimit = overrides.gasLimit ?? estimateIntrinsicGas(calls, accessList);

@randygrok randygrok marked this pull request as ready for review February 2, 2026 10:10
@randygrok randygrok requested a review from a team as a code owner February 2, 2026 10:10
Copy link

@chatton chatton left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice over all looking good!

A few things and then I think we can merge if the E2Es are passing

  • can we add some unit tests and CI for the client (first set can be minimal I think, something we can add to)
  • update the package.json to allow for running the basic/flows/sponsored tests.
  • Add a basic linter or formatter in the package.json also (and run in CI)

@randygrok
Copy link
Contributor Author

merging to main, next step will be to checkout this into the impl branch

#103

@randygrok randygrok merged commit 933d335 into main Feb 10, 2026
16 checks passed
@randygrok randygrok deleted the randygrok/client-viem-ev-reth branch February 10, 2026 10:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] Implement Viem-based custom client for ADR-0003: Typed Transaction 0x76 Sponsorship

3 participants